/*
 * $Id: slm.c,v 1.7 2006-02-06 15:54:00 bacon Exp $
 */

#include <xp/utl/slm.h>
#include <xp/bas/ctype.h>
#include <xp/bas/string.h>
#include <xp/bas/stdlib.h>
#include <xp/bas/memory.h>
#include <xp/net/inet.h>

static int __parse_pri (const xp_char_t** ptr, xp_slm_t* slm, int year);
static int __parse_time (const xp_char_t** ptr, xp_slm_t* slm, int year);
static int __parse_host (const xp_char_t** ptr, xp_slm_t* slm, int year);
static int __parse_tag (const xp_char_t** ptr, xp_slm_t* slm, int year);
static int __parse_extra_pri (const xp_char_t** ptr, xp_slm_t* slm, int year);
static int __parse_text (const xp_char_t** ptr, xp_slm_t* slm, int year);

static int __str_to_number (const xp_char_t* tok, xp_size_t tok_len);
static int __month_to_number (const xp_char_t* tok, xp_size_t tok_len);
static int __fac_to_number (const xp_char_t* str, xp_size_t len);
static int __sev_to_number (const xp_char_t* str, xp_size_t len);

int (*__parse_func[])(const xp_char_t**, xp_slm_t*, int year) =
{
	__parse_pri,
	__parse_time,
	__parse_host,
	__parse_tag,
	__parse_extra_pri,
	__parse_text
};

enum 
{
	PS_PRI = 0,
	PS_TIME,
	PS_HOST,
	PS_TAG,
	PS_EXTRAPRI,
	PS_TEXT,
	PS_END
};

/* syslog message facilities 

    0             kernel messages
    1             user-level messages
    2             mail system
    3             system daemons
    4             security/authorization messages (note 1)

    5             messages generated internally by syslogd
    6             line printer subsystem
    7             network news subsystem
    8             UUCP subsystem
    9             clock daemon (note 2)
   10             security/authorization messages (note 1)
   11             FTP daemon
   12             NTP subsystem
   13             log audit (note 1)
   14             log alert (note 1)
   15             clock daemon (note 2)
   16             local use 0  (local0)
   17             local use 1  (local1)
   18             local use 2  (local2)
   19             local use 3  (local3)
   20             local use 4  (local4)
   21             local use 5  (local5)
   22             local use 6  (local6)
   23             local use 7  (local7)

   Note 1 - Various operating systems have been found to utilize
            Facilities 4, 10, 13 and 14 for security/authorization,
            audit, and alert messages which seem to be similar.
   Note 2 - Various operating systems have been found to utilize
            both Facilities 9 and 15 for clock (cron/at) messages.
 */

static const xp_char_t* __facname[] =
{
	XP_TEXT("kern"),
	XP_TEXT("user"),
	XP_TEXT("mail"),
	XP_TEXT("daemon"),
	XP_TEXT("auth,security"),
	XP_TEXT("syslog"),
	XP_TEXT("lpr"),
	XP_TEXT("news"),
	XP_TEXT("uucp"),
	XP_TEXT("cron"),
	XP_TEXT("authpriv"),
	XP_TEXT("ftp"),
	XP_TEXT("ntp"),
	XP_TEXT("logaudit"),
	XP_TEXT("logalert"),
	XP_TEXT("clock"),
	XP_TEXT("local0"),
	XP_TEXT("local1"),
	XP_TEXT("local2"),
	XP_TEXT("local3"),
	XP_TEXT("local4"),
	XP_TEXT("local5"),
	XP_TEXT("local6"),
	XP_TEXT("local7"),
	XP_TEXT("remoteauth"),
	XP_TEXT("netinfo"),
	XP_NULL
};

static const xp_char_t* __sevname[] =
{
	XP_TEXT("emerg,panic"),
	XP_TEXT("alert"),
	XP_TEXT("crit"),
	XP_TEXT("err,error"),
	XP_TEXT("warning,warn"),
	XP_TEXT("notice"),
	XP_TEXT("info"),
	XP_TEXT("debug"),
	XP_NULL
};

int xp_parse_slm (const xp_char_t* src, xp_slm_t* slm, int year)
{
	const xp_char_t* p = src, * oldp;
	int state = PS_PRI, n;

	if (p == XP_NULL || p[0] == XP_CHAR('\0')) return -1;

	slm->fldmask = 0; 

	while (state != PS_END) {
		oldp = p;

		n = (__parse_func[state]) (&p, slm, year);

		if (n == 0) {
			state++;
			if (state != PS_END && 
			    (p == XP_NULL || p[0] == XP_CHAR('\0'))) p = oldp;
		}
		else if (n > 0) {
			/* syntatically correct, but wrong value */
			p = oldp;	
			/*ps = PS_TEXT;	*/
			state++;
		}
		else {    
			if (state == PS_TEXT) return -1;
			p = oldp;
			state++;
		}	
	}

	return 0;
}

static int __parse_pri (const xp_char_t** ptr, xp_slm_t* slm, int year)
{
#define SKIP_SPACES while(xp_isspace(*p)) p++;

	const xp_char_t* p = *ptr;
	int pri;

	if (p == XP_NULL) return -1;

	/* i don't skip spaces here which means a valid priority 
	 * must not have any proceeding space characters
	 * SKIP_SPACES; 
	 */
	if (*p != XP_CHAR('<')) return -1;
	p++;

	SKIP_SPACES;

	// a priority number must come after <.
	if (!xp_isdigit(*p)) return -1; 

	pri = 0;
	while (xp_isdigit(*p)) {
		pri = pri * 10 + (*p - XP_CHAR('0'));
		p++;
	}

	SKIP_SPACES;
	/* a priority is generally enclosed inside <>.
	 * it seems a missing > is not really a problem.
	 */
	if (*p == XP_CHAR('>')) p++;

	/* valid priorities must be followed by other parts of the message */
	if (p[0] == XP_CHAR('\0')) return -1;

	/* The priority part is corret in systax but its value might 
	 * be wrong. So let the parser skip the priority as long as 
	 * it is corrent syntatically.
	 */
	*ptr = p;
	
	/* The Priority value is calculated by first multiplying the Facility
	 * number by 8 and then adding the numerical value of the Severity
	 * e.g.) pri = fac << 3 | sev
	 */
	slm->pri.fac = pri >> 3;
	slm->pri.sev = pri & 0x07;

	/* invalid priority values are ignored */
	if (slm->pri.fac < xp_countof(__facname) - 1) {
		slm->fldmask |= XP_SLM_FLD_PRI;
	}

	return 0;
}

static int __parse_time (const xp_char_t** ptr, xp_slm_t* slm, int year)
{
	const xp_char_t* p = *ptr;

	int mon, day, hour, min, sec;
	xp_char_t* tok;
	xp_size_t tok_len;
	xp_tm_t tm;

	/* month */
	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(""), &tok, &tok_len);
	if (tok_len == 0) return -1;
	mon = __month_to_number (tok, tok_len);
	if (mon == -1) return -1;

	/* day */
	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(""), &tok, &tok_len);
	if (tok_len == 0) return -1;
	if (!xp_strxistype (tok, tok_len, xp_isdigit)) return -1;
	day = __str_to_number (tok, tok_len);

	/* hour */
	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(":"), &tok, &tok_len);
	if (tok_len == 0) return -1;
	if (!xp_strxistype (tok, tok_len, xp_isdigit)) return -1;
	hour = __str_to_number (tok, tok_len);

	/* minute */
	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(":"), &tok, &tok_len);
	if (tok_len == 0) return -1;
	if (!xp_strxistype (tok, tok_len, xp_isdigit)) return -1;
	min = __str_to_number (tok, tok_len);

	/* second */
	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(""), &tok, &tok_len);
	if (tok_len == 0) return -1;
	if (!xp_strxistype (tok, tok_len, xp_isdigit)) return -1;
	sec = __str_to_number (tok, tok_len);

	/* a valid timestamp is also followed by other parts of the message */
	if (p == XP_NULL || p[0] == XP_CHAR('\0')) return -1;

	*ptr = p;

	xp_memset (&tm, 0, xp_sizeof(tm));
	tm.tm_year = year - XP_TIME_BASE_YEAR;
	tm.tm_mon  = mon - 1;
	tm.tm_mday = day;
	tm.tm_hour = hour;
	tm.tm_min  = min;
	tm.tm_sec  = sec;

	if (xp_mktime(&tm, &slm->time) == -1) return -1;
	slm->fldmask |= XP_SLM_FLD_TIME;

	return 0;
}

int __parse_host (const xp_char_t** ptr, xp_slm_t* slm, int year)
{
	const xp_char_t* p = *ptr;
	xp_char_t* tok;
	xp_size_t tok_len;

	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(""), &tok, &tok_len);
	if (tok_len == 0) return -1;

	/* a too long name is not regarded as a valid host name */
	if (tok_len > XP_SLM_MAX_HOST) return -1;

	/* valid hostnames don't end with a colon. */
	if (tok[tok_len - 1] == XP_CHAR(':')) return -1; 

	/* neither - nor -- is not a valid hostname */
	if (tok_len == 1 && tok[0] == XP_CHAR('-')) return -1;
	if (tok_len == 2 && 
	    tok[0] == XP_CHAR('-') && tok[1] == XP_CHAR('-')) return -1;

	/* a valid hostname is followed by other parts of the message */
	if (p == XP_NULL || p[0] == XP_CHAR('\0')) return -1;

	if (tok_len > 2 && tok[0] == XP_CHAR('[') && 
	    tok[tok_len - 1] == XP_CHAR(']')) {
		/* Solaris - unresolved ip of forwarded messages
		 * [192.168.2.3.2.3]
		 */
		xp_char_t ip[3 + 1 + 3 + 1 + 3 + 1 + 3 + 1];
		xp_size_t pos = 1, dot_cnt = 0, ipl = 0;
		while (pos < tok_len - 1) {
			if (tok[pos] == XP_CHAR('.')) {
				dot_cnt++;
				if ((dot_cnt == 5 && pos == tok_len - 2) || dot_cnt > 5) {
					dot_cnt = 0;
					break;
				}
			}
			else if (!xp_isdigit(tok[pos])) {
				dot_cnt = 0;
				break;
			}

			if (dot_cnt < 4) {
				ip[ipl++] = tok[pos];
				if (ipl >= xp_countof(ip) - 1) break;
			}
			pos++;
		}
		if (dot_cnt == 5) {
			xp_uint32_t nip;

			ip[ipl] = XP_CHAR('\0');
			if (xp_inet4_strtoip(ip, &nip) == 0) {
				tok = &tok[1];
				tok_len = ipl;
			}
		}
	}

	slm->fldmask |= XP_SLM_FLD_HOST;
	xp_strxncpy (slm->host, xp_countof(slm->host), tok, tok_len);

	*ptr = p;
	return 0;
}

int __parse_tag (const xp_char_t** ptr, xp_slm_t* slm, int year)
{
	const xp_char_t* p = *ptr;
	xp_char_t* tok;
	xp_size_t tok_len;

	xp_char_t* tag, * pid;
	xp_size_t tag_len, pid_len;
	int pid_state;
	xp_size_t i;

	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(""), &tok, &tok_len);
	/*
	 * Some Linux login logs have a double-dash before a program name.
	 * Dec 26 21:58:34 fuck  -- bacon[32569]: LOGIN ON pts/1 BY 
	 * bacon FROM 211.200.75.85
	 */
	if (tok_len == 2 && tok[0] == XP_CHAR('-') && tok[1] == XP_CHAR('-')) {
		if (p == XP_NULL) return -1;
		p = xp_strtok (p, XP_TEXT(""), &tok, &tok_len);
	}
	if (tok_len <= 1) return -1;

	/* a valid program name is followed by other parts of the message */
	if (p == XP_NULL || p[0] == XP_CHAR('\0')) return -1;

	tag = tok;
	tag_len = 0;

	pid_state = 0;
	pid = XP_NULL;
	pid_len = 0;

	for (i = 0; i < tok_len; i++) {
		if (pid_state == 0) {
			if (tok[i] == XP_CHAR('[')) {
				if (i == 0) {
					/* a bracket is the first character of the tag */
					break;
				}
				pid = &tok[i + 1];
				pid_state = 1;
			}
			else if (tok[i] == XP_CHAR(']')) break;
			else if (tok[i] == XP_CHAR(':')) {
				if (i != tok_len - 1) {
					/* a clone is not the last character of the tag */
					break;
				}
				pid_state = 3;
			}
			else if (!xp_isprint(tok[i])) break;
			else { 
				/* program name too long */
				if (i >= XP_SLM_MAX_TAG) break;
				tag_len++;
			}
		}
		else if (pid_state == 1) {
			/* in pid now */

			if (tok[i] == XP_CHAR(']')) { 
				/*if (tok[i - 1] == XP_CHAR('[')) {*/
				if (pid_len == 0) {
					/* empty pid */
					break;
				}
				pid_state = 2;	

				XP_STRXTOI (slm->pid, pid, pid_len, XP_NULL, 10);
				slm->fldmask |= XP_SLM_FLD_PID;
			}
			else { 
				if (!xp_isdigit(tok[i])) {
					/* pid is not numeric */
					break;
				}
				pid_len++;
			}
		}
		else if (pid_state == 2) {
			if (tok[i] != XP_CHAR(':')) break;
			pid_state = 3;
		}
	}

	if (pid_state != 3) return -1;

	slm->fldmask |= XP_SLM_FLD_TAG;
	xp_strxncpy (slm->tag, xp_countof(slm->tag), tag, tag_len);

	*ptr = p;
	return 0;
}

int __parse_extra_pri (const xp_char_t** ptr, xp_slm_t* slm, int year)
{
	const xp_char_t* p = *ptr;
	xp_char_t* tok;
	xp_size_t tok_len;
	int fac, sev;

	/* when the priority is specified at the head of the message and
	 * it doesn't match an extra priority, the extra priority becomes
	 * a part of the message text.
	 * if (slm->fldmask & XP_SLM_FLD_PRI) return -1;
	 */

	/* Solaris writes a message id when it's enabled 
	 * [ID msgid facility.severity]
	 * e.g. [ID 702911 user.notice] 
	 */ 
	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(""), &tok, &tok_len);
	if (tok_len != 3 || (tok[0] != XP_CHAR('[') && 
	    tok[1] != XP_CHAR('I') && tok[2] != XP_CHAR('D'))) return -1;

	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(""), &tok, &tok_len);
	if (!xp_strxistype(tok, tok_len, xp_isdigit)) return -1;

	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT("."), &tok, &tok_len);
	fac = __fac_to_number (tok, tok_len);
	if (fac == -1) return -1;
	
	if (p == XP_NULL) return -1;
	p = xp_strtok (p, XP_TEXT(""), &tok, &tok_len);
	if (tok_len <= 0 || tok[tok_len - 1] != XP_CHAR(']')) return -1;
	sev = __sev_to_number (tok, tok_len - 1);
	if (sev == -1) return -1;

	if (slm->fldmask & XP_SLM_FLD_PRI) {
		if (slm->pri.fac != fac || slm->pri.sev != sev) {
			return -1;
		}
	}
	else {
		slm->pri.fac = fac;
		slm->pri.sev = sev;
	}

	// additional priority infomation is followed by the 
	// actual text of the message.
	if (p == XP_NULL || p[0] == XP_CHAR('\0')) return -1;

	slm->fldmask |= XP_SLM_FLD_PRI;
	*ptr = p;
	return 0;
}

int __parse_text (const xp_char_t** ptr, xp_slm_t* slm, int year)
{
	const xp_char_t* p = *ptr;
	xp_char_t* text = slm->text;
	xp_size_t tsz = xp_countof(slm->text);

	if (p == XP_NULL) text[0] = XP_CHAR('\0');
	else {
		xp_size_t i = 0;
		while (*p != XP_CHAR('\0') && i < tsz - 1) {
			text[i++] = *p++;
			if (i > XP_SLM_MAX_TEXT) return -1;
		}
		text[i] = XP_CHAR('\0');
		while (*p++ != XP_CHAR('\0')) {
			if (++i > XP_SLM_MAX_TEXT) return -1;
		}
	}

	slm->fldmask |= XP_SLM_FLD_TEXT;
	*ptr = XP_NULL;
	return 0;
}

static int __str_to_number (const xp_char_t* tok, xp_size_t tok_len)
{
	int day;
	XP_STRXTOI (day, tok, tok_len, XP_NULL, 10);
	return day;
}

struct mtab 
{
	const xp_char_t* name;
	int value;
} __mtab[] =
{
	{ XP_TEXT("Jan"),   1},
	{ XP_TEXT("Feb"),   2},
	{ XP_TEXT("Mar"),   3},
	{ XP_TEXT("Apr"),   4},
	{ XP_TEXT("May"),   5},
	{ XP_TEXT("Jun"),   6},
	{ XP_TEXT("Jul"),   7},
	{ XP_TEXT("Aug"),   8},
	{ XP_TEXT("Sep"),   9},
	{ XP_TEXT("Oct"),  10},
	{ XP_TEXT("Nov"),  11},
	{ XP_TEXT("Dec"),  12},

	{ XP_TEXT("January"),    1},
	{ XP_TEXT("February"),   2},
	{ XP_TEXT("March"),      3},
	{ XP_TEXT("April"),      4},
	{ XP_TEXT("May"),        5},
	{ XP_TEXT("June"),       6},
	{ XP_TEXT("July"),       7},
	{ XP_TEXT("August"),     8},
	{ XP_TEXT("September"),  9},
	{ XP_TEXT("October"),   10},
	{ XP_TEXT("November"),  11},
	{ XP_TEXT("December"),  12},

	{XP_NULL,     0}
};

static int __month_to_number (const xp_char_t* str, xp_size_t len)
{
	struct mtab* p = __mtab;

	for (p = __mtab; p->name != XP_NULL; p++) {
		if (xp_strxcasecmp(str, len, p->name) == 0) return p->value;
	}

	return -1;
}

static int __fac_to_number (const xp_char_t* str, xp_size_t len)
{
	const xp_char_t** p;
	const xp_char_t* pp;
	xp_char_t* tok;
	xp_size_t tok_len;

	for (p = __facname; *p != XP_NULL; p++) {
		pp = *p;
		while (pp != XP_NULL) {
			pp = xp_strtok (pp, XP_TEXT(","), &tok, &tok_len);
			if (tok_len > 0 && 
			    xp_strxncmp(tok, tok_len, str, len) == 0) 
				return (int)(p - __facname);
		}
	}

	return -1;
}

static int __sev_to_number (const xp_char_t* str, xp_size_t len)
{
	const xp_char_t** p;
	const xp_char_t* pp;
	xp_char_t* tok;
	xp_size_t tok_len;

	for (p = __sevname; *p != XP_NULL; p++) {
		pp = *p;
		while (pp != XP_NULL) {
			pp = xp_strtok (pp, XP_TEXT(","), &tok, &tok_len);
			if (tok_len > 0 && 
			    xp_strxncmp(tok, tok_len, str, len) == 0) 
				return (int)(p - __sevname);
		}
	}

	return -1;
}
